package com.jfxia.arkles;

import javafx.data.pull.Event;
import java.lang.Exception;


/**
 * Binds an event tree structure with an XPath like location.
 */
public class Trigger {
	/**
	 * The XPath that will trigger this event tree.
	 */
	public-init var path:String;
	/**
	 * The root node of the event tree to use when this trigger is met.
	 */
	public-init var eventNode:EventNode;
	
	var pathNodes:PathNode[];
	
	/**
	 * Convert string path into structure.
	 */
	init {
		pathNodes = createPathNodes(path);
	}
	
	/**
	 * Match this trigger to a given event path (a list of <code>PullParser</code>
	 * <code>Event</code> objects, starting at the document root).
	 * Does this trigger path match the event path passed in?  Must be
	 * an exact match, with everything in trigger path matching event path.
	 */
	public function test(ep:Event[]) : Boolean {
		_db1(ep);
			
		def epSz:Integer = sizeof ep;
		
		if(epSz==0) { _db2("1"); return false; }
		
		var epIdx:Integer = 0;
		for(pn:PathNode in pathNodes) {
			// Search path longer than event path?
			if(epIdx>=epSz) { _db2("2"); return false; }
			// Check trigger path node against event path
			if(pn.descend) {
				_db2("Descend");
				// Descend down even path looking for node matching trigger node
				while((epIdx < epSz) and (not pn.test(ep[epIdx]))) {
					epIdx++;
				}
				// Did we overshoot the event path, without matching?
				if(epIdx>=epSz) { _db2("3"); return false; }
				// Must has found a match; move to next event node
				epIdx++;
			} else {
				_db2("Plain {pn.name} => {ep[epIdx].qname.name}");
				// Does this trigger node match the event node?
				if(not pn.test(ep[epIdx])) { _db2("4"); return false; }
				// Yes it does, move to next event node
				epIdx++;
			}
		}
		
		// Everything matched, but did out path fully match the
		// events passed in?
		_db2("Exit: {epIdx}=={sizeof ep}");
		return (epIdx == sizeof ep);
	}
	
	/**
	 * Turns a path string into a <code>PathNode</code> structure,
	 * for speedier checking inside <code>test()</code>.
	 */
	function createPathNodes(p:String) : PathNode[] {
		def slash:Character = '/'.charAt(0);
		
		var start:Integer = 0;
		var end:Integer = start;
		def len:Integer = p.length();
		
		var seq:PathNode[] = [];
		while(start < len) {
			var pn:PathNode = PathNode {}
			// First char should be a slash
			if(p.charAt(start) != slash) {
				throw new Exception("Illegal start of XPath term, missing slash: {p}");
			}
			start++;
			// If the next char is another slash, set descend flag
			if((start < len) and (p.charAt(start) == slash)) {
				pn.descend=true;
				start++;
			}
			// Extract rest of term
			end=start;
			while((end < len) and (p.charAt(end) != slash)) {
				end++;
			}
			var term:String = p.substring(start,end);
			start = end;
			// Parse term
			def attrStart:Integer = term.indexOf("[");
			if(attrStart >= 0) {
				// Format: name[@attr] OR name[@attr='value']
				pn.name = term.substring(0 , attrStart);
				term = term.substring(attrStart);
				// Check for [@ ... ] pattern
				if(not (term.startsWith("[@") and term.endsWith("]")))
					throw new Exception("Malformed attribute: {p}");				
				// Strip [@ ... ] bookends
				term = term.substring(2,term.length()-1);				
				// Attr contains a value?
				def eq:Integer = term.indexOf("='");
				if(eq >= 0) {
					pn.attrName = term.substring(0,eq);
					term = term.substring(eq+1);
					pn.attrValue = term.substring(1,term.length()-1);
				} else {
					pn.attrName = term;
				}
			} else {
				// Format: name
				pn.name = term;
			}
			
			if(pn.name.length()==0)
				throw new Exception("Missing name: {p}");
			
			insert pn into seq;
		}
		return seq;
	}
	
	override function toString() : String {
		var s:String="";
		for(pn in pathNodes)
			s = "{s}{pn.toString()}";
		return s;
	}

	// DEBUG
	def debug:Boolean = false;
	function _db1(ev:Event[]) {
		if(not debug) { return; }
		print("Comparing {this.toString()} against ");
		for(e:Event in ev) { print("/{e.qname.name}"); }
		println("");			
	}
	function _db2(s:String) {
		if(not debug) { return; }
		println("  {s}");
	}
}

/**
 * Represents one node on a trigger search path.
 */
class PathNode
{	public var descend:Boolean = false;
	public var name:String;
	public var attrName:String;
	public var attrValue:String;
	
	override function toString() : String {
		var s:String ="/{if(descend) '/' else ''}{name}";
		if(attrName!=null) {
			s="{s}[@{attrName}";
			if(attrValue!=null) {
				s="{s}='{attrValue}'";
			}
			s="{s}]";
		}
		return s;
	}
	
	/**
	 * Returns true if this <code>PathNode</code> matches the 
	 * <code>Event</code> passed in.
	 */
	function test(ev:Event) : Boolean {
		// Do names match?
		if(ev.qname.name != name) { return false; }
		// Is the required attribute present?
		if(attrName != null) {
			var b:Boolean = false;
			for(n in ev.getAttributeNames()) {
				if(n.toString().equals(attrName)) {
					b = true;
					break;
				}
			}
			if(not b) { return false; }
		}
		// Is the required attribute value present?
		if(attrValue != null) {
			def v:String = ev.getAttributeValue(attrName);
			if(v == null) { return false; }
			if(not attrValue.equals(v)) { return false; }
		}
		// Everything matched
		return true;
	}
}